<?php
/**
 * 类、函数库
 */

/**
 * 返回值接口
 */
interface IResponse {
    function getResponse($mValues);
    function getErrorResponse($iCode, $sMessage);
}

/**
 * 返回值接口的工厂
 */
class ResponseFactory {
    public static function create($sFormat) {
        $oRetval = null;

        switch($sFormat) {
            case 'json':
                $oRetval = new JSONResponse();
            default:
                $oRetval = null;
        }

        return $oRetval;
    }
}

/**
 * 返回JSON
 */
class JSONResponse implements IResponse {
    public function getResponse($mValues) {
        return json_encode($mValues);
    }
    public function getErrorResponse($iCode, $sMessage) {
        $aResponse = array(
            'code'=>$iCode,
            'message'=>$sMessage
        );
        return json_encode($aResponse);
    }
}


/**
 * 将Query字符串解析为Query对象
 */
class QueryParser {
    public function parse($sQuery) {
        $aChars = $this->getUTF8Chars($sQuery);

        list($aPriorityArray, $iUseless) = $this->getPriorityArray($aChars, count($aChars), 0);
        $nPriorityArray = count($aPriorityArray);
        if($nPriorityArray == 0) {
            return null;
        } else if($nPriorityArray == 1) {
            return new XapianQuery(base64_encode($aPriorityArray[0]));
        }

        $aParsedTree = $this->getParsedTree($aPriorityArray);
        return $this->getQueryByTree($aParsedTree);
    }

    protected function getUTF8Chars($s) {
        preg_match_all("/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|\xe0[\xa0-\xbf][\x80-\xbf]|[\xe1-\xef][\x80-\xbf][\x80-\xbf]|\xf0[\x90-\xbf][\x80-\xbf][\x80-\xbf]|[\xf1-\xf7][\x80-\xbf][\x80-\xbf][\x80-\xbf]/", $s, $a);  

        return $a[0];
    }

    protected function getPriorityArray($aChars, $nChars, $iPos = 0) {
        $aRetval = array();

        $nChars = count($aChars);
        for(;$iPos<$nChars;) {
            list($sWord, $iPos) = $this->getWord($aChars, $nChars, $iPos);

            switch($sWord) {
                case "(":
                    list($aNextLevel, $iPos) = $this->getPriorityArray($aChars, $nChars, $iPos);
                    $aRetval[] = $aNextLevel;
                    break;
                case ")":
                    return array($aRetval, $iPos);
                default:
                    $aRetval[] = $sWord;
                    break;
            }
        }
        

        return array($aRetval, $iPos);
    }

    protected function getWord($aChars, $nChars, $iPos = 0) {
        $sRetval = '';

        for(;$iPos<$nChars;$iPos++) {
            $c = $aChars[$iPos];
            switch($c) {
                case "(":
                case ")":
                    if($sRetval) {
                        return array($sRetval, $iPos);
                    } else { // ( or )
                        return array($c, $iPos+1);
                    }
                case "&":
                case "|":
                    if($aChars[$iPos+1] == $c) {
                        if($sRetval) {
                            return array($sRetval, $iPos);
                        } else { // && or ||
                            return array($c.$c, $iPos+2);
                        }
                    } else { // normal
                        $sRetval .= $c;
                    }
                    break;
                default:
                    $sRetval .= $c;
                    break;
            }
        }

        return array($sRetval, $nChars);
    }

    protected function getParsedTree($aPriorityArray) {
        $aRetval = array();

        $nPriorityArray = count($aPriorityArray);
        for($i=1;$i<$nPriorityArray;) {
            $aTree = array();
            if(!isset($aPriorityArray[$i])) {
                throw new Exception("Syntax error");
            }
            if(!isset($aPriorityArray[$i+1])) {
                throw new Exception("Syntax error");
            }
            if($aPriorityArray[$i] != "&&"
                && $aPriorityArray[$i] != "||") {
                throw new Exception("Syntax error");
            }

            $aTree['value'] = $aPriorityArray[$i];

            if(is_array($aPriorityArray[$i+1])) {
                $aTree['right'] = $this->getParsedTree($aPriorityArray[$i+1]);
            } else {
                $aTree['right'] = $aPriorityArray[$i+1];
            }

            if($i == 1) {
                if(is_array($aPriorityArray[$i-1])) {
                    $aTree['left'] = $this->getParsedTree($aPriorityArray[$i-1]);
                } else {
                    $aTree['left'] = $aPriorityArray[$i-1];
                }
            } else {
                $aTree['left'] = $aRetval;
            }

            $aRetval = $aTree;
            $i+=2;
        }

        return $aRetval;
    }

    protected function getQueryByTree($aParsedTree) {
        if(is_array($aParsedTree['left'])) {
            $oLeft = $this->getQueryByTree($aParsedTree['left']);
        } else {
            $oLeft = $this->getQueryByString($aParsedTree['left']);
        }
        if(is_array($aParsedTree['right'])) {
            $oRight = $this->getQueryByTree($aParsedTree['right']);
        } else {
            $oRight = $this->getQueryByString($aParsedTree['right']);
        }
        switch($aParsedTree['value']) {
            case "&&":
                $iOperator = XapianQuery::OP_AND;
                break;
            case "||":
                $iOperator = XapianQuery::OP_OR;
                break;
            default:
                throw new Exception("Syntax error");
        }

        return new XapianQuery($iOperator, $oLeft, $oRight);
    }

    protected function getQueryByString($s) {
        $a = explode(":", $s);
        $n = count($a);
        $sPrefix = $sKeyword = "";
        switch($n) {
            case 0:
            default:
                throw new Exception("Syntax error");
            case 1:
                $sKeyword = $s;
                break;
            case 2:
                $sPrefix = $a[0];
                $sKeyword = $a[1];
                break;
        }

        $oQuery = null;
        $aTerms = WordSegmentation::segmentation($sKeyword);
        if($aTerms) foreach($aTerms as $t) {
            $sTerm = $sPrefix . base64_encode($t);

            if(!$oQuery) {
                $oQuery = new XapianQuery($sTerm);
            } else {
                $oQuery = new XapianQuery(XapianQuery::OP_AND, $oQuery, new XapianQuery($sTerm));
            }
        }

        return $oQuery;
    }
}


/**
 * 主线程
 */
class MainThread {
    protected $aWorkerThreads = array();
    protected $bTerminating = false;
    public function run() {
        // import config
        $sCustomConfigPath = BIN_ROOT . '/../opt/simaqian.php';
        if(file_exists($sCustomConfigPath)) {
            require $sCustomConfigPath;
        } else {
            $sDefaultConfigPath = BIN_ROOT . '/../etc/simaqian.php';
            if(file_exists($sDefaultConfigPath)) {
                require $sDefaultConfigPath;
            } else {
                print "Config file is missing\n";
            }
        }


        // start worker(s)
        foreach($GLOBALS['DataStores'] as $ds => $v) {
            $oWorkerThread = new WorkerThread($ds);
            $oWorkerThread->start();
            $this->aWorkerThreads[] = $oWorkerThread;
        }

        // register signal
        pcntl_signal(SIGTERM, array(&$this, 'signalHandler'));

        // wait for terminating
        while(!$this->bTerminating) {
            sleep(1);
        }
    }

    public function signalHandler($signo) {
        switch ($signo) {
            case SIGTERM:
                if($this->aWorkerThreads) foreach($this->aWorkerThreads as $w) {
                    $w->stop();
                    unset($w);
                }

                $this->bTerminating = true;
                break;
        }
    }
}

/**
 * 工作者线程
 */
class WorkerThread {
    public function __construct($sDataStoreName) {
        $this->sDataStoreName = $sDataStoreName;
    }

    protected $sDataStoreName = "";
    protected $oXiancheng = null;
    protected $bTerminating = false;

    public function start() {
        if($this->oXiancheng) {
            throw new Exception("Cannot start process for the second time");
        }

        $this->oXiancheng = new PHPXiancheng(array(&$this, "proc"));
        $this->oXiancheng->start();
    }

    public function stop() {
        if($this->oXiancheng) {
            $this->oXiancheng->stop();
            unset($this->oXiancheng);
        }
    }

    public function signalHandler($signo) {
        switch ($signo) {
            case SIGTERM:
                $this->bTerminating = true;
                break;
        }
    }

    public function proc() {
        // register signal
        pcntl_signal(SIGTERM, array(&$this, 'signalHandler'));

        // rizhi
        $oErrorRizhi = new PHPRizhi(BIN_ROOT . "/../var/simaqian/simaqian.%Y%m%d.err");

        // duilie
        $oDuilie = new PHPDuilie('simaqian_' . $this->sDataStoreName . '_cmds');
        $oDuilie->addQServer($GLOBALS['MemcacheQServer']['host'], $GLOBALS['MemcacheQServer']['port']);
        $oDuilie->addDBServer($GLOBALS['MemcacheDBServer']['host'], $GLOBALS['MemcacheDBServer']['port']);

        while(!$this->bTerminating) {
            $aCmd = $oDuilie->dequeue();
            if(!$aCmd) {// no more
                sleep(3);
                continue;
            }

            if(!isset($aCmd['cmd'])) {// no command
                continue;
            }

            switch($aCmd['cmd']) {
                case 'add':
                    $this->addProc();
                    break;
                case 'delete':
                    $this->deleteProc();
                    break;
                case 'replace':
                    $this->replaceProc();
                    break;
                default:// unknown command
                    break;
            }
        }
    }

    protected function addProc($aCmd) {
        if(!isset($aCmd['key'])
            || !isset($aCmd['values'])) {// need whole parameters
            return;
        }

        // new document
        $oDocument = new XapianDocument();
        $iTermPosition = 1;
        
        // index
        $aFields = $GLOBALS['DataStores'][$this->sDataStoreName]['fields'];
        if($aFields) foreach($aFields as $f => $attr) {
            $oIndexer = IndexerFactory::create($attr);
            $iTermPosition = $oIndexer->index($oDocument, $iTermPosition, $aCmd['values'][$f]);
        }

        // key
        $oDocument->add_value(0, $aCmd['key']);

        // add
        $iDocumentId = IndexDatabase::addDocument($this->sDataStoreName, $oDocument);
        IndexDatabase::flush($this->sDataStoreName);

        // record map
        $oMC = new Memcache();
        $oMC->addServer($GLOBALS['MemcacheDBServer']['host'], $GLOBALS['MemcacheDBServer']['port']);
        $oMC->set('simaqian_' . $this->sDataStoreName . '_' . $aCmd['key'] . '_document_id', $iDocumentId);
        unset($oMC);
    }

    protected function deleteProc($aCmd) {
        if(!isset($aCmd['key'])) {// need whole parameters
            return;
        }

        // get document id
        $oMC = new Memcache();
        $oMC->addServer($GLOBALS['MemcacheDBServer']['host'], $GLOBALS['MemcacheDBServer']['port']);
        $iDocumentId = $oMC->get('simaqian_' . $this->sDataStoreName . '_' . $aCmd['key'] . '_document_id');


        // delete
        IndexDatabase::deleteDocument($this->sDataStoreName, $iDocumentId);
        IndexDatabase::flush($this->sDataStoreName);

        
        $oMC->delete('simaqian_' . $this->sDataStoreName . '_' . $aCmd['key'] . '_document_id');
        unset($oMC);
    }

    protected function replaceProc($aCmd) {
        $this->deleteProc($aCmd);
        $this->addProc($aCmd);
    }
}


class WordSegmentation {
    protected static $oSCWS = null;
    public static function segmentation($s) {
        if(!self::$oSCWS) {
            self::$oSCWS = scws_open();
        }

        $aRetval = array();
        scws_send_text(self::$oSCWS, strip_tags($s));
        while($aLines = scws_get_result(self::$oSCWS)) {
            if($aLines) foreach($aLines as $aWord) {
                $aRetval[] = $aWord['word'];
            }
        }
        return $aRetval;
    }

    public static function close() {
        if(self::$oSCWS) {
            scws_close(self::$oSCWS);
            unset(self::$oSCWS);
        }
    }
}

class IndexDatabase {
    protected static $aDBs = array();
    public static function addDocument($sDataStore, &$oDocument) {
        if(!isset(self::$aDBs[$sDataStore])) {
            $sDatabasePath = $GLOBALS['DataStores'][$sDataStore]['path'];
            self::$aDBs[$sDataStore] = new XapianWritableDatabase($sDatabasePath, Xapian::DB_CREATE_OR_OPEN);
        }

        return self::$aDBs[$sDataStore]->add_document($oDocument);
    }

    public static function deleteDocument($sDataStore, $iDocumentId) {
        if(!isset(self::$aDBs[$sDataStore])) {
            $sDatabasePath = $GLOBALS['DataStores'][$sDataStore]['path'];
            self::$aDBs[$sDataStore] = new XapianWritableDatabase($sDatabasePath, Xapian::DB_CREATE_OR_OPEN);
        }

        return self::$aDBs[$sDataStore]->delete_document($iDocumentId);
    }

    public static function flush($sDataStore) {
        if(!isset(self::$aDBs[$sDataStore])) {
            $sDatabasePath = $GLOBALS['DataStores'][$sDataStore]['path'];
            self::$aDBs[$sDataStore] = new XapianWritableDatabase($sDatabasePath, Xapian::DB_CREATE_OR_OPEN);
        }

        self::$aDBs[$sDataStore]->flush();
    }

    public static function close($sDataStore) {
        unset(self::$aDBs[$sDataStore]);
    }
}


interface IIndexer {
    function index(&$oDocument, $iTermPosition, $mValue);
}

class IndexerFactory {
    public static function create($aFieldAttrs) {
        $oRetval = null;

        switch($aFieldAttrs['indexer']) {
            case 'DefaultIndexer':
                $oRetval = new DefaultIndexer(isset($aFieldAttrs['weight'])?$aFieldAttrs['weight']:1);
                break;
            case 'TermIndexer':
                $oRetval = new TermIndexer(isset($aFieldAttrs['prefix'])?$aFieldAttrs['prefix']:'');
                break;
            case 'MonthIndexer':
                $oRetval = new MonthIndexer(isset($aFieldAttrs['prefix'])?$aFieldAttrs['prefix']:'');
                break;
        }

        return $oRetval;
    }
}

class DefaultIndexer implements IIndexer {
    public function __construct($iWeight = 1) {
        $this->iWeight = $iWeight;
    }

    protected $iWeight;

    public function index(&$oDocument, $iTermPosition, $mValue) {
        $aTerms = WordSegmentation::segmentation($mValue);
        if($aTerms) foreach($aTerms as $t) {
            $oDocument->add_posting(base64_encode($t), $iTermPosition++, $this->iWeight);
        }

        return $iTermPosition;
    }
}

class TermIndexer implements IIndexer {
    public function __construct($sPrefix = '') {
        $this->sPrefix = $sPrefix;
    }

    protected $sPrefix;

    public function index(&$oDocument, $iTermPosition, $mValue) {
        $aTerms = WordSegmentation::segmentation($mValue);
        if($aTerms) foreach($aTerms as $t) {
            $oDocument->add_posting($this->sPrefix . base64_encode($t), $iTermPosition++);
        }

        return $iTermPosition;
    }
}

class MonthIndexer implements IIndexer {
    public function __construct($sPrefix = '') {
        $this->sPrefix = $sPrefix;
    }

    protected $sPrefix;

    public function index(&$oDocument, $iTermPosition, $mValue) {
        $oDocument->add_posting($this->sPrefix . base64_encode(strftime("%Y%m", $mValue)), $iTermPosition++);

        return $iTermPosition;
    }
}

?>